const Koa = require('koa');
const app = new Koa();
const route = require('koa-route');
const path = require('path');
const serve = require('koa-static');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const jwtKoa = require('koa-jwt');
const koaBody = require('koa-body');
const fs = require('fs');

const website = {
    scheme: 'http',
    host: 'localhost',
    port: 1337,
    join: function () {
        return `${this.scheme}://${this.host}:${this.port}`
    }
}

/* 数据库 */
// 连接数据库
let contest;
{
    const mongodb = {
        scheme: 'mongodb',
        host: process.env.MONGO_HOST ? process.env.MONGO_HOST : 'localhost',
        port: process.env.MONGO_PORT ? process.env.MONGO_PORT : 27017,
        database: 'contest',
        join: function () {
            return `${this.scheme}://mongoadmin:mongopass@${this.host}:${this.port}/${this.database}?authSource=admin`;
        }
    }
    mongoose.connect(`${mongodb.join()}`);
    contest = mongoose.connection;
    contest.on('error', console.error.bind(console, 'error'));
    contest.once('open', function () {
        console.log(`${mongodb.join()} 数据库连接成功！`);
    });
}
// 数据库文档架构
const topicSchema = mongoose.Schema({
    question: String,
    answer: String,
    score: Number
});
const Topic = mongoose.model('topics', topicSchema);
const userSchema = mongoose.Schema({
    name: String,
    schoolNumber: Number
});
const User = mongoose.model('users', userSchema);
const adminSchema = mongoose.Schema({
    schoolNumber: Number
});
const Admin = mongoose.model('admins', userSchema);
const rankingsSchema = mongoose.Schema({
    name: String,
    schoolNumber: Number,
    results: Number,
    haveAnswered: Object,
});
const Rankings = mongoose.model('rankings', rankingsSchema);

/* jwt密钥 */
const secret = 'codeRush';

/* 未认证处理 */
app.use((ctx, next) => {
    return next().catch((err) => {
        if (err.status === 401) {
            ctx.status = 401;
            ctx.body = {
                ok: false,
                msg: err.originalError ? err.originalError.message : err.message
            }
        } else {
            throw err;
        }
    });
});

/* 路由权限控制 */
app.use(jwtKoa({ secret: secret }).unless({
    path: [
        /^\/api\/login/,
        /^\/api\/register/,
        /^((?!\/api).)*$/
    ]
}));

/* 解析 Request body 到 ctx.request.body */
app.use(koaBody());

/* POST /api/register 注册 */
app.use(route.post('/api/register', async (ctx, next) => {
    const body = xss(ctx.request.body);
    // 判断是否填写
    if (body.name === undefined || body.schoolNumber === undefined) {
        return ctx.body = {
            ok: false,
            msg: '姓名或者学号，没有填写'
        }
    }
    // 判断是否已经注册
    if (await isRegister({ schoolNumber: body.schoolNumber })) {
        return ctx.body = {
            ok: false,
            msg: '你已经注册过了'
        }
    }

    const newUser = new User({
        name: body.name,
        schoolNumber: body.schoolNumber
    });
    const saveResult = await saveUser({ doc: newUser });
    if (saveResult) {
        return ctx.body = {
            ok: true,
            msg: '注册成功',
            token: getToken({ name: body.name, schoolNumber: body.schoolNumber, admin: await isAdmin(body.schoolNumber) })
        }
    } else {
        return ctx.body = {
            ok: false,
            msg: '注册失败'
        }
    }

}));
function saveUser({ doc = null } = {}) {
    return new Promise((resolve, reject) => {
        if (doc === null) {
            resolve(false);
        }
        doc.save(function (err, val) {
            if (err) {
                reject(err);
            } else {
                resolve(val);
            }
        });
    });
}
function isRegister({ schoolNumber = null } = {}) {
    return new Promise((resolve, reject) => {
        // 没有填写参数,肯定查找不到,所以直接返回false
        if (schoolNumber === null) {
            resolve(false);
        }
        User.find({ schoolNumber: schoolNumber }, function (err, val) {
            if (err) {
                reject(err);
            } else {
                if (val.length) {
                    resolve(true);
                } else {
                    resolve(false);
                }
            }
        });
    });
}

/* GET /api/login 登录 */
app.use(route.get('/api/login', async (ctx, next) => {
    let queryJSON = xss(ctx.request.query);
    // 判断是否填写
    if (queryJSON.name === undefined || queryJSON.schoolNumber === undefined) {
        return ctx.body = {
            ok: false,
            msg: '姓名或者学号，没有填写'
        }
    }
    // 判断是否已经注册
    if (!await isRegister({ schoolNumber: queryJSON.schoolNumber })) {
        return ctx.body = {
            ok: false,
            msg: '你还没有注册'
        }
    }
    // 判断姓名、学号是否正确
    if (!await isPassworldCorrect({ name: queryJSON.name, schoolNumber: queryJSON.schoolNumber })) {
        return ctx.body = {
            ok: false,
            msg: '姓名或者学号不正确'
        }
    }
    return ctx.body = {
        ok: true,
        msg: '登录成功',
        token: getToken({ name: queryJSON.name, schoolNumber: queryJSON.schoolNumber, admin: await isAdmin(queryJSON.schoolNumber) })
    }
}));
function isPassworldCorrect({ name = null, schoolNumber = null } = {}) {
    return new Promise((resolve, reject) => {
        // 没有填写参数,肯定查找不到,所以直接返回false
        if (name === null || schoolNumber === null) {
            resolve(false);
        }
        User.find({ name: name, schoolNumber: schoolNumber }, function (err, val) {
            if (err) {
                reject(err);
            } else {
                if (val.length) {
                    resolve(true);
                } else {
                    resolve(false);
                }
            }
        });
    });
}
function getToken(payload = {}) {
    return jwt.sign(payload, secret, { expiresIn: '4h' });
}
function isAdmin(schoolNumber = null) {
    return new Promise((resolve, reject) => {
        // 没有填写参数,肯定查找不到,所以直接返回false
        if (schoolNumber === null) {
            resolve(false);
        }
        Admin.find({ schoolNumber: schoolNumber }, function (err, val) {
            if (err) {
                reject(err);
            } else {
                if (val.length) {
                    resolve(true);
                } else {
                    resolve(false);
                }
            }
        });
    });
}

/* POST /api/topic 新建题目 */
app.use(route.post('/api/topic', async (ctx, next) => {
    const body = xss(ctx.request.body);
    // 判断题目各项，是否填写完整
    if (body.question === undefined || body.answer === undefined || body.score === undefined) {
        return ctx.body = {
            ok: false,
            msg: '问题、答案、分值，没有填写完整'
        }
    }
    // 判断是不是管理员
    if (!await isAdmin(getJWTPayload(ctx.headers.authorization).schoolNumber)) {
        return ctx.body = {
            ok: false,
            msg: '不是管理员，不能新建题目'
        }
    }
    // 保存题目
    const newTopic = new Topic({
        question: body.question,
        answer: body.answer,
        score: Number(body.score)
    })
    const saveResult = await saveUser({ doc: newTopic });
    if (saveResult) {
        return ctx.body = {
            ok: true,
            msg: '新建题目成功'
        }
    } else {
        return ctx.body = {
            ok: false,
            msg: err
        }
    }
}));
function saveDoc({ doc }) {
    return new Promise((resolve, reject) => {
        doc.save(function (err, val) {
            if (err) {
                reject(err);
            } else {
                resolve(val);
            }
        });
    }).catch((err) => {
        console.log(err);
        return false;
    });
}
function getJWTPayload(token) {
    return jwt.verify(token.split(' ')[1], secret);
}

/* GET /api/topic 获取题目 */
app.use(route.get('/api/topic', async (ctx, next) => {
    const data = await getTopics()
        .catch(function (err) {
            return false;
        });
    if (data) {
        return ctx.body = {
            ok: true,
            msg: '获取题目成功',
            data: data
        }
    } else {
        return ctx.body = {
            ok: false,
            msg: '获取题目失败'
        }
    }
}));
function getTopics() {
    return new Promise((resolve, reject) => {
        Topic.find({}, '_id question score', function (err, docs) {
            if (err) {
                reject(err);
            } else {
                resolve(docs);
            }
        });
    });
}

/* PUT /api/answer 回答问题 只允许在指定时间范围提交答案 */
app.use(route.put('/api/answer', async (ctx, next) => {
    let contestConf;
    try {
        contestConf = JSON.parse(fs.readFileSync(path.join(__dirname, 'conf', 'contest.json')).toString());
        let nbf = new Date(contestConf.nbf).getTime();
        let exp = new Date(contestConf.exp).getTime();
        let cur = new Date().getTime();
        if (cur >= nbf && cur <= exp) {
            await next();
        } else {
            return ctx.body = {
                ok: false,
                msg: '不在比赛时间，不能提交答案'
            }
        }
    } catch (e) {
        return ctx.body = {
            msg: e
        }
    }
}));

/* PUT /api/answer 回答问题 */
app.use(route.put('/api/answer', async (ctx, next) => {
    const body = xss(ctx.request.body);
    if (body.topicid === undefined || body.answer === undefined) {
        return ctx.body = {
            ok: false,
            msg: '没有题目id或者没有回答'
        }
    }
    // 获取参赛者答题情况
    payloadJSON = getJWTPayload(ctx.headers.authorization)
    const situation = await new Promise((resolve, reject) => {
        Rankings.findOne({ schoolNumber: payloadJSON.schoolNumber }, (err, docs) => {
            if (err) {
                reject(err);
            } else {
                resolve(docs);
            }
        });
    }).catch((err) => {
        return ctx.body = {
            ok: false,
            msg: err
        }
    });

    if (null !== situation && 'ok' in situation) {
        return ctx.body = situation;
    } else {
        // 获取提交的题目信息
        const topicInfo = await findOne({
            model: Topic,
            conditions: { _id: body.topicid }
        });
        if (!topicInfo) {
            return ctx.body = {
                ok: false,
                msg: '找不到提交的题目'
            }
        }
        // 如果没有数据，则新插入一条
        if (null === situation) {
            const answer = new Rankings({
                name: payloadJSON.name,
                schoolNumber: payloadJSON.schoolNumber,
                results: 0,
                haveAnswered: {}
            });

            answer.haveAnswered[body.topicid] = {
                score: topicInfo.score,
                answer: body.answer,
                rightAnswer: topicInfo.answer,
                correct: body.answer === topicInfo.answer
            }

            if (answer.haveAnswered[body.topicid].correct) {
                answer.results = answer.haveAnswered[body.topicid].score;
            }

            if (await saveDoc({ doc: answer })) {
                return ctx.body = {
                    ok: true,
                    msg: '提交答案成功'
                }
            } else {
                return ctx.body = {
                    ok: false,
                    msg: '提交答案失败'
                }
            }
        } else {

            situation.haveAnswered[body.topicid] = {
                score: topicInfo.score,
                answer: body.answer,
                rightAnswer: topicInfo.answer,
                correct: body.answer === topicInfo.answer
            }

            situation.results = 0;
            for (let v in situation.haveAnswered) {
                situation.results += situation.haveAnswered[v].correct ? situation.haveAnswered[v].score : 0;
            }

            const ok = update({
                model: Rankings,
                conditions: { schoolNumber: payloadJSON.schoolNumber },
                update: { $set: { results: situation.results, haveAnswered: situation.haveAnswered } }
            });

            if (ok) {
                return ctx.body = {
                    ok: true,
                    msg: '提交答案成功'
                }
            } else {
                return ctx.body = {
                    ok: false,
                    msg: '提交答案失败'
                }
            }
        }
    }
}));
function findOne({ model, conditions = {}, filter = '' }) {
    return new Promise((resolve, reject) => {
        model.findOne(conditions, filter, (err, doc) => {
            if (err) {
                reject(err);
            } else {
                resolve(doc);
            }
        });
    }).catch((err) => {
        console.log(err);
        return false;
    });
}
function update({ model, conditions = {}, update = {} }) {
    return new Promise((resolve, reject) => {
        model.update(conditions, update, (err) => {
            if (err) {
                reject(err);
            } else {
                resolve(true);
            }
        });
    }).catch((err) => {
        console.error(err);
        return false;
    });
}

/* GET /api/rankings 排行榜 */
app.use(route.get('/api/rankings', async (ctx, next) => {
    const result = await find({
        model: Rankings,
        fields: '-_id name schoolNumber results',
        options: { sort: { results: -1 } }
    });
    if (result) {
        return ctx.body = {
            ok: true,
            msg: '获取排行榜成功',
            data: result
        }
    } else {
        return ctx.body = {
            ok: false,
            msg: '获取排行榜失败'
        }
    }
}));
function find({ model, conditions = {}, fields = '', options = {} }) {
    return new Promise((resolve, reject) => {
        model.find(conditions, fields, options, (err, docs) => {
            if (err) {
                reject(err);
            } else {
                resolve(docs);
            }
        });
    }).catch((err) => {
        console.log(err);
        return false;
    });
}

/* 重定向主页 login.html */
app.use(route.get('/', (ctx, next) => {
    ctx.status = 301;
    ctx.redirect('/html/user/login.html');
}));
function xss(s) {
    s = JSON.stringify(s)
    s = s.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\$/g, '');
    return JSON.parse(s);
}

/* 静态资源处理 */
app.use(serve(path.join(__dirname, 'static')));
/* 监听服务器端口 */
app.listen(website.port, () => {
    console.log(`${website.join()} 服务器已经启动！`);
});